#!/usr/bin/perl -w
#
# $Id: Locator.pm,v 1.16 2003/08/04 04:57:02 solovam Exp $
#
# This file is a part of gkismet
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#

#
# Locator class
#
package Locator;

use Gnome;
use Math::Trig;
use Math::Complex;
use Misc;
use BssConnObserver;
use DrawingArea;
use strict;
@Locator::ISA = qw(BssConnObserver);

my $minHeadingDist = 10;
my $compassWidth = 400;
my $compassHeight = 60;
my @currCoordLabels = ('Longitude', 'Latitude', 'Bearing', 'Fix');
my @netCoordLabels = ('Longitude', 'Latitude', 'Distance', 'Bearing');

#
# Constructor
#
sub new
{
	my $type = shift;
	my $self = new BssConnObserver(@_);
	bless $self, $type;

	# Check if there are GPS data
	if(!defined $self->{'connection'}->getGps()->{'lat'} || $self->{'connection'}->getNetwork()->{$self->{'bssid'}}{'aggpoints'} == 0)
	{
		my $messagebox = new Gnome::MessageBox("No GPS data for network bssid: " . $self->{'bssid'}, 'error', 'Button_Ok');
		$messagebox->run_and_close();
		return undef;
	}

	# Init vars
	$self->{'mode'} = 'averageCenter';
	$self->{'lastLat'} = $self->{'connection'}->getGps()->{'lat'}; 
	$self->{'lastLon'} = $self->{'connection'}->getGps()->{'lon'}; 
	$self->{'headingAngle'} = 0;
	$self->recalculate();

	# Window
	my $window = new Gtk::Window('dialog');
	$window->signal_connect("delete_event", sub {$self->deleteHandler()});
	$window->set_title('Network locator ' . $self->{'connection'}->getNetwork()->{$self->{'bssid'}}{'ssid'} . ' ' . $self->{'bssid'});
	$self->{'window'} = $window;

	# Drawing area
	$self->{'drawingArea'} = new DrawingArea($compassWidth, $compassHeight, sub {$self->drawCompass()});
	my $compassFrame = new Gtk::Frame(undef);
	$compassFrame->add($self->{'drawingArea'}->getWidget());
	my $fixed = new Gtk::Fixed();
	$fixed->put($compassFrame, 0, 0);
	my $compassHbox= new Gtk::HBox($true, 0);
	$compassHbox->pack_start($fixed, $true, $false, 20);

	# Labels
	my $currCoordFrame = new Gtk::Frame(undef);
	$self->{'currCoordFrame'} = $currCoordFrame;
	$currCoordFrame->set_label('Current coordinates');
	my $currCoordVbox = new Gtk::VBox($false, 5);
	$currCoordFrame->add($currCoordVbox);
	for my $l (@currCoordLabels)
	{
		my $hbox = new Gtk::HBox($false, 1);

		my $nlabel = new Gtk::Label($l . ': ');
		$nlabel->set_justify('left');
		$hbox->pack_start($nlabel, $false, $false, 5);

		my $vlabel = new Gtk::Label('');
		$vlabel->set_justify('right');
		$hbox->pack_end($vlabel, $false, $false, 5);

		$currCoordVbox->pack_start($hbox, $false, $false, 3);
        
		$self->{'currCoordLabel'}{$l} = $vlabel;
	}
	my $netCoordFrame = new Gtk::Frame(undef);
	$self->{'netCoordFrame'} = $netCoordFrame;
	$netCoordFrame->set_label('Average network center');
	my $netCoordVbox = new Gtk::VBox($false, 5);
	$netCoordFrame->add($netCoordVbox);
	for my $l (@netCoordLabels)
	{
		my $hbox = new Gtk::HBox($false, 1);

		my $nlabel = new Gtk::Label($l . ': ');
		$nlabel->set_justify('left');
		$hbox->pack_start($nlabel, $false, $false, 5);

		my $vlabel = new Gtk::Label('');
		$vlabel->set_justify('right');
		$hbox->pack_end($vlabel, $false, $false, 5);

		$netCoordVbox->pack_start($hbox, $false, $false, 3);
        
		$self->{'netCoordLabel'}{$l} = $vlabel;
	}
	$self->updateLabels();

	# Buttons
	my $bbox = new Gtk::HButtonBox();
	$bbox->set_layout('spread');
	$bbox->set_spacing(5);
	my $buttonClose = new Gtk::Button("Close");
	$buttonClose->signal_connect( "clicked", sub {$self->closeClicked()});
	$bbox->add($buttonClose);
	my $buttonMode = new Gtk::Button("Mode");
	$buttonMode->signal_connect( "clicked", sub {$self->modeClicked($buttonMode)});
	$bbox->add($buttonMode);

	# Assemble
	my $hbox = new Gtk::HBox($true, 0);
	$hbox->pack_start($currCoordFrame, $true, $true, 20);
	$hbox->pack_start($netCoordFrame, $true, $true, 20);
	my $vbox = new Gtk::VBox($false, 10);
	$vbox->pack_start($compassHbox, $true, $false, 20);
	$vbox->pack_start($hbox, $true, $false, 0);
	$vbox->pack_start($bbox, $true, $false, 20);
	$window->add($vbox);
	$window->show_all();

	# Start updates
	$self->{'connection'}->addObserver($self);

	return $self;
}

#
# Update labels
#
sub updateLabels
{
	my $self = shift;
	$self->{'currCoordLabel'}{'Latitude'}->set_text(sprintf "%.6f", $self->{'lat'});
	$self->{'currCoordLabel'}{'Longitude'}->set_text(sprintf "%.6f", $self->{'lon'});
	$self->{'currCoordLabel'}{'Bearing'}->set_text(sprintf "%.0f", $self->{'headingAngle'});
	my $fix;
	if($self->{'fix'} == -1)
	{
		$fix = 'No signal';
	}
	elsif($self->{'fix'} == 2)
	{
		$fix = '2D';
	}
	elsif($self->{'fix'} == 3)
	{
		$fix = '3D';
	}
	else
	{
		$fix = 'NONE';
	}
	$self->{'currCoordLabel'}{'Fix'}->set_text($fix);
	$self->{'netCoordLabel'}{'Latitude'}->set_text(sprintf "%.6f", $self->{'centerLat'});
	$self->{'netCoordLabel'}{'Longitude'}->set_text(sprintf "%.6f", $self->{'centerLon'});
	$self->{'netCoordLabel'}{'Bearing'}->set_text(sprintf "%.0f", $self->{'centerAngle'});
	my $dist = '';
	if($self->{'gKismetApplication'}->{'preferences'}->getPref('units') eq 'metric')
	{
		if($self->{'dist'} < 1000)
		{
			$dist = sprintf("%.2fm", $self->{'dist'});
		}
		else
		{
			$dist = sprintf("%.3fkm", $self->{'dist'} / 1000);
		}
	}
	else
	{
		if($self->{'dist'} / $mileFactor > 0.5)
		{
			$dist = sprintf("%.3fmi", $self->{'dist'} / $mileFactor);
		}
		else
		{
			$dist = sprintf("%.2fft", $self->{'dist'} / $footFactor);
		}
	}
	$self->{'netCoordLabel'}{'Distance'}->set_text($dist);
}

#
# Change network locator mode
#
sub modeClicked
{
	my $self = shift;
	if($self->{'mode'} eq 'averageCenter')
	{
		$self->{'mode'} = 'strongestSignal';
		$self->{'netCoordFrame'}->set_label('Strongest signal point');
	}
	elsif($self->{'mode'} eq 'strongestSignal')
	{
		$self->{'mode'} = 'averageCenter';
		$self->{'netCoordFrame'}->set_label('Average network center');
	}
	$self->recalculate();
	$self->drawCompass();
	$self->updateLabels();
}

#
# Draw our compass into the pixmap
#
sub drawCompass
{
	my $self = shift;

	# Abbreviations
	my $pixmap = $self->{'drawingArea'}->pixmap();
	my $gtkDrawingArea = $self->{'drawingArea'}->gtkDrawingArea();

	# Backdrop
	$pixmap->draw_rectangle($gtkDrawingArea->style()->black_gc(), $true,
		0, 0, $gtkDrawingArea->allocation()->[2], $gtkDrawingArea->allocation()->[3]);

	# Middle horizontal white line
	$pixmap->draw_line($gtkDrawingArea->style()->white_gc(), 0, $compassHeight/2, $compassWidth, $compassHeight/2);

	# Red vertical line
	$pixmap->draw_line($self->{'drawingArea'}->{'redGC'}, $compassWidth/2, 0, $compassWidth/2, $compassHeight);

	# Digits on the gauge every 30 degrees
	for(my $i = 0; $i < 12; $i++)
	{
		# Linear position of a mark
		my $l = $self->deg2pix($i * 30);
		# Draw a mark
		$pixmap->draw_line($gtkDrawingArea->style()->white_gc(), $l, $compassHeight / 2 + 5, $l, $compassHeight / 2 - 5);
		# Draw a label
		$pixmap->draw_string($gtkDrawingArea->style()->font(), $gtkDrawingArea->style()->white_gc(),
			# Shift text left by half of string length
			$l - $self->{'drawingArea'}->stringWidth($i * 30) / 2,
			# Shift text down from the middle by mark height (5) + padding (5) + string height
			$compassHeight / 2 + 5 + $self->{'drawingArea'}->stringHeight($i * 30) + 5,
			$i * 30);
	}

	# North, south, west, east letters. Pretty mach same as above
	for my $i ([0, 'N'], [90, 'E'], [180, 'S'], [270, 'W'])
	{
		my $l = $self->deg2pix($i->[0]);
		$pixmap->draw_string($gtkDrawingArea->style()->font(), $gtkDrawingArea->style()->white_gc(),
			$l - $self->{'drawingArea'}->stringWidth($i->[1]) / 2,
			$compassHeight / 2 - 5 - 5,
			$i->[1]);
	}

	# Linear position of the center angle mark
	my $l = $self->deg2pix($self->{'centerAngle'});
	# A little triangle
	my @triangle = ($l - 5, $compassHeight, $l, $compassHeight - 5, $l + 5, $compassHeight);
	$pixmap->draw_polygon($self->{'drawingArea'}->{'redGC'}, $true, @triangle);

	# Redraw us
	$gtkDrawingArea->draw();

	return;
}

#
# Translate degress into pixels on our compass scale
#
sub deg2pix
{
	my $self = shift;
	my $deg = shift;

	my $l = ($deg - $self->{'headingAngle'}) * $compassWidth / 360 + $compassWidth * 3/2;

	while ($l >= $compassWidth)
	{
		$l -= $compassWidth;
	}

	return $l;
}

#
# Handle an update from an observable
#
sub update
{
	my $self = shift;
	my $object = shift;
	my $data = shift;

	if($object->isa('Connection') && defined $self->{'connection'} && $self->{'connection'} eq $object)
	{
		if($data->{'changed'} eq 'gps' || ($data->{'changed'} eq 'network' && $data->{'bssid'} eq $self->{'bssid'}))
		{
			$self->recalculate();
			$self->drawCompass();
			$self->updateLabels();
		}
	}
}

#
# Repopulate coordinates, recalculate bearings and distance
#
sub recalculate
{
	my $self = shift;

	if($self->{'mode'} eq 'averageCenter')
	{
		$self->{'centerLat'} = $self->{'connection'}->getNetwork()->{$self->{'bssid'}}{'agglat'} /
			$self->{'connection'}->getNetwork()->{$self->{'bssid'}}{'aggpoints'}; 
		$self->{'centerLon'} = $self->{'connection'}->getNetwork()->{$self->{'bssid'}}{'agglon'} /
			$self->{'connection'}->getNetwork()->{$self->{'bssid'}}{'aggpoints'};
	}
	elsif($self->{'mode'} eq 'strongestSignal')
	{
		$self->{'centerLat'} = $self->{'connection'}->getNetwork()->{$self->{'bssid'}}{'bestlat'};
		$self->{'centerLon'} = $self->{'connection'}->getNetwork()->{$self->{'bssid'}}{'bestlon'};
	}

	$self->{'lat'} = $self->{'connection'}->getGps()->{'lat'};
	$self->{'lon'} = $self->{'connection'}->getGps()->{'lon'};
	$self->{'fix'} = $self->{'connection'}->getGps()->{'fix'};
	
	$self->{'centerAngle'} = Locator->calcAngle($self->{'lat'}, $self->{'lon'}, $self->{'centerLat'}, $self->{'centerLon'});
	$self->{'dist'} = Locator->earthDistance($self->{'lat'}, $self->{'lon'}, $self->{'centerLat'}, $self->{'centerLon'});

	if(Locator->earthDistance($self->{'lastLat'}, $self->{'lastLon'}, $self->{'lat'}, $self->{'lon'}) >= $minHeadingDist)
	{
		$self->{'headingAngle'} = Locator->calcAngle($self->{'lastLat'}, $self->{'lastLon'}, $self->{'lat'}, $self->{'lon'});
		$self->{'lastLat'} = $self->{'lat'};
		$self->{'lastLon'} = $self->{'lon'};
	}
}

#
# Get Gtk widget
#
sub getWidget
{
	my $self = shift;
	return $self->{'window'};
}

#
# Distance from the center of Earth in meters
#
# Taken (with some mods) from Kismet
#
sub calcRad
{
	my $type = shift;
	my $lat = shift;
	$lat = deg2rad($lat);
	
	# the radius of curvature of an ellipsoidal Earth in the plane of the
	# meridian is given by
	#
	# R' = a * (1 - e^2) / (1 - e^2 * (sin(lat))^2)^(3/2)
	#
	# where a is the equatorial radius,
	# b is the polar radius, and
	# e is the eccentricity of the ellipsoid = sqrt(1 - b^2/a^2)
	#
	# a = 6378 km (3963 mi) Equatorial radius (surface to center distance)
	# b = 6356.752 km (3950 mi) Polar radius (surface to center distance)
	# e = 0.081082 Eccentricity

	my $a = 6378.137;
	my $e2 = 0.081082 * 0.081082;
	my $sc = sin($lat);
	my $x = $a * (1.0 - $e2);
	my $z = 1.0 - $e2 * $sc * $sc;
	my $y = Misc->pow($z, 1.5);
	my $r = $x / $y;
	$r = $r * 1000;

	return $r;
}

#
# Distance between points in meters
#
# Taken (with some mods) from Kismet
#
sub earthDistance
{
	my $type = shift;
	my ($lat1, $lon1, $lat2, $lon2) = (@_);
	my $x1 = Locator->calcRad($lat1) * cos(deg2rad($lon1)) * sin(deg2rad(90 - $lat1));
	my $x2 = Locator->calcRad($lat2) * cos(deg2rad($lon2)) * sin(deg2rad(90 - $lat2));
	my $y1 = Locator->calcRad($lat1) * sin(deg2rad($lon1)) * sin(deg2rad(90 - $lat1));
	my $y2 = Locator->calcRad($lat2) * sin(deg2rad($lon2)) * sin(deg2rad(90 - $lat2));
	my $z1 = Locator->calcRad($lat1) * cos(deg2rad(90 - $lat1));
	my $z2 = Locator->calcRad($lat2) * cos(deg2rad(90 - $lat2));
	my $a = acos(($x1 * $x2 + $y1 * $y2 + $z1 * $z2) / Misc->pow(Locator->calcRad(($lat1 + $lat2) / 2), 2));
	my $dist = Locator->calcRad(($lat1 + $lat2) / 2) * $a;
	if(ref($dist) eq 'Math::Complex')
	{
		return 0;
	}
	return $dist;
}

#
# Azimuth
#
# Taken (with some mods) from Kismet
#
sub calcAngle
{
	my $type = shift;
	my($lat1, $lon1, $lat2, $lon2) = (@_);

	my $R = Locator->calcRad($lat2);

	$lat1 = deg2rad($lat1);
	$lon1 = deg2rad($lon1);
	$lat2 = deg2rad($lat2);
	$lon2 = deg2rad($lon2);

	my $angle;
	# we are moving along parallel
	if($lat1 == $lat2)
	{
		# east
		if($lon2 > $lon1)
		{
			$angle = pi/2;
		}
		# west
		elsif($lon2 < $lon1)
		{
			$angle = 3 * pi / 2;
		}
		# not moving at all
		else
		{
			return(0);
		}
	}
	# we are moving along meridian
	elsif($lon1 == $lon2)
	{
		# north
		if($lat2 > $lat1)
		{
			$angle = 0;
		}
		# south
 		elsif($lat2 < $lat1)
		{
			$angle = pi;
		}
		# we never get here
		else
		{
			return(undef);
		}
	}
	# otherwise we calculate heading
	else
	{
		my $tx = $R * cos($lat1) * ($lon2 - $lon1);
		my $ty = $R * ($lat2 - $lat1);
		$angle = atan($tx / $ty);

		if ($ty < 0)
		{
			$angle += pi;
		}
		if($angle >= (2 * pi))
		{
			$angle -= 2 * pi;
		}
		if($angle < 0)
		{
			$angle += 2 * pi;
		}
	}

	return rad2deg($angle);
}

1;
